//////////////////////////////////////////////
// main.cpp
//
//////////////////////////////////////////////

/// Includes ---------------------------------

// Local
#include "PastCode.h"

// nkGraphics
#include <NilkinsGraphics/Compositors/Compositor.h>
#include <NilkinsGraphics/Compositors/CompositorManager.h>
#include <NilkinsGraphics/Compositors/CompositorNode.h>
#include <NilkinsGraphics/Compositors/TargetOperations.h>

#include <NilkinsGraphics/Passes/ClearTargetsPass.h>
#include <NilkinsGraphics/Passes/PostProcessPass.h>
#include <NilkinsGraphics/Passes/RenderScenePass.h>

#include <NilkinsGraphics/RenderContexts/RenderContext.h>
#include <NilkinsGraphics/RenderContexts/RenderContextDescriptor.h>
#include <NilkinsGraphics/RenderContexts/RenderContextManager.h>

/// Internals : Texture ----------------------

void prepareTexture ()
{
	// This time, the function we will load will be a cubemap
	nkGraphics::Texture* tex = nkGraphics::TextureManager::getInstance()->createOrRetrieve("tex") ;

	tex->setPath("yokohamaNight.dds") ;
	tex->setGammaCorrected(false) ;
	tex->load() ;
}

/// Internals : Shader -----------------------

void prepareProgram ()
{
	// Our program will be very similar to last time : only need to sample a cubemap through the right class and direction
	nkGraphics::Program* program = nkGraphics::ProgramManager::getInstance()->createOrRetrieve("program") ;
	nkGraphics::ProgramSourcesHolder sources ;

	sources.setVertexMemory
	(
		R"eos(
			cbuffer PassBuffer : register(b0)
			{
				matrix view ;
				matrix proj ;
				float3 camPos ;
			}

			struct VertexInput
			{
				float4 position : POSITION ;
				float3 normal : NORMAL ;
				matrix world : WORLD_MATRIX ;
			} ;

			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float3 normal : NORMAL ;
				float3 camDir : CAMDIR ;
			} ;

			PixelInput main (VertexInput input)
			{
				PixelInput result ;

				matrix mvp = mul(proj, mul(view, input.world)) ;
				result.position = mul(mvp, input.position) ;
				result.normal = input.normal ;
				result.camDir = normalize(mul(input.world, input.position) - camPos) ;

				return result ;
			}
		)eos"
	) ;

	sources.setPixelMemory
	(
		R"eos(
			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float3 normal : NORMAL ;
				float3 camDir : CAMDIR ;
			} ;

			TextureCube tex : register(t0) ;
			SamplerState customSampler : register(s0) ;
				
			float4 main (PixelInput input) : SV_TARGET
			{
				float3 sampleDir = reflect(input.camDir, normalize(input.normal)) ;
				return tex.Sample(customSampler, sampleDir) ;
			}
		)eos"
	) ;

	program->setFromMemory(sources) ;
	program->load() ;
}

void prepareEnvProgram ()
{
	// This program will be used to render a background in sync with the sphere rendered
	nkGraphics::Program* envProgram = nkGraphics::ProgramManager::getInstance()->createOrRetrieve("envProgram") ;
	nkGraphics::ProgramSourcesHolder envSources ;

	envSources.setVertexMemory
	(
		R"eos(
			// The camDir entry is an array of 4 float4
			// They will correspond to the 4 corners of the screen
			// This is true because we will use this shader in a post process pass
			// Such a pass map a quad to the screen and renders it using the shader provided
			cbuffer constants
			{
				float4 camDir [4] ;
			}

			// The vertexId is fed by DirectX and correspond to the index of the vertex being processed
			// A pass slot for camera direction feeds the corners in sync with the order the vertices are put in the mesh
			// As such, we can use it to directly index which direction a vertex corresponds to
			struct VertexInput
			{
				float4 position : POSITION ;
				uint vertexId : SV_VertexID ;
			} ;

			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float4 camDir : CAMDIR ;
			} ;

			PixelInput main (VertexInput input)
			{
				PixelInput result ;

				result.position = input.position ;
				result.camDir = camDir[input.vertexId] ;

				return result ;
			}
		)eos"
	) ;

	envSources.setPixelMemory
	(
		R"eos(
			// The camera direction will be interpolated between the 4 vertices, for each pixel
			// This means we will get the direction for a given pixel in the world
			// Using that knowledge we only need to sample the cube map
			struct PixelInput
			{
				float4 position : SV_POSITION ;
				float4 camDir : CAMDIR ;
			} ;

			TextureCube envMap : register(t0) ;
			SamplerState customSampler : register(s0) ;

			float4 main (PixelInput input) : SV_TARGET
			{
				return envMap.Sample(customSampler, normalize(input.camDir)) ;
			}
		)eos"
	) ;

	envProgram->setFromMemory(envSources) ;
	envProgram->load() ;
}

void prepareShader ()
{
	// Prepare the shader which will be used by the sphere
	nkGraphics::Shader* shader = nkGraphics::ShaderManager::getInstance()->createOrRetrieve("shader") ;
	nkGraphics::Program* program = nkGraphics::ProgramManager::getInstance()->get("program") ;

	shader->setProgram(program) ;

	nkGraphics::ConstantBuffer* cBuffer = shader->addConstantBuffer(0) ;

	nkGraphics::ShaderPassMemorySlot* slot = cBuffer->addPassMemorySlot() ;
	slot->setAsViewMatrix() ;

	slot = cBuffer->addPassMemorySlot() ;
	slot->setAsProjectionMatrix() ;

	slot = cBuffer->addPassMemorySlot() ;
	slot->setAsCameraPosition() ;

	nkGraphics::ShaderInstanceMemorySlot* instanceSlot = shader->addInstanceMemorySlot() ;
	instanceSlot->setAsWorldMatrix() ;

	// Prepare for tex and sampler
	nkGraphics::Texture* tex = nkGraphics::TextureManager::getInstance()->get("tex") ;
	nkGraphics::Sampler* sampler = nkGraphics::SamplerManager::getInstance()->get("sampler") ;

	shader->addTexture(tex, 0) ;
	shader->addSampler(sampler, 0) ;

	shader->load() ;
}

void prepareEnvShader ()
{
	// Prepare the post process shader
	nkGraphics::Program* envProgram = nkGraphics::ProgramManager::getInstance()->get("envProgram") ;

	nkGraphics::Texture* tex = nkGraphics::TextureManager::getInstance()->get("tex") ;
	nkGraphics::Sampler* sampler = nkGraphics::SamplerManager::getInstance()->get("sampler") ;

	nkGraphics::Shader* envShader = nkGraphics::ShaderManager::getInstance()->createOrRetrieve("envShader") ;
	
	envShader->setProgram(envProgram) ;

	nkGraphics::ConstantBuffer* cBuffer = envShader->addConstantBuffer(0) ;

	nkGraphics::ShaderPassMemorySlot* slot = cBuffer->addPassMemorySlot() ;
	slot->setAsCamCornersWorld() ;

	envShader->addTexture(tex, 0) ;
	envShader->addSampler(sampler, 0) ;

	envShader->load() ;
}

/// Internals : Compositor -------------------

nkGraphics::Compositor* prepareCompositor ()
{
	// Compositor drives the composition of the image
	nkGraphics::Shader* envShader = nkGraphics::ShaderManager::getInstance()->get("envShader") ;

	// We first need to retrieve the compositor through its manager
	// Compositor is composed of nodes that can be turned on or off at will
	// Those nodes define target operation, to know which targets they should operate on
	// Finally, the operations feature passes, which will be executed sequentially
	nkGraphics::Compositor* compositor = nkGraphics::CompositorManager::getInstance()->createOrRetrieve("compositor") ;
	nkGraphics::CompositorNode* node = compositor->addNode() ;
	nkGraphics::TargetOperations* targetOp = node->addOperations() ;

	// Render to the context's buffers
	// The back buffer is the one corresponding the the window's surface
	targetOp->setToBackBuffer(true, 0) ;
	// The chain depth buffer is a buffer furnished by the context for ease of use
	targetOp->setToChainDepthBuffer(true) ;

	// First clear both targets, through the clear targets pass
	nkGraphics::ClearTargetsPass* clearPass = targetOp->addClearTargetsPass() ;

	// Then render the render queue through the render scene pass
	nkGraphics::RenderScenePass* scenePass = targetOp->addRenderScenePass() ;

	// Final step, post process the whole image, using the environment shader we created
	// Post process passes map a quad on the screen, and execute the shader provided
	// A back process pass will only color pixel that are not touched by any geometry
	nkGraphics::PostProcessPass* postProcessPass = targetOp->addPostProcessPass() ;
	postProcessPass->setBackProcess(true) ;
	postProcessPass->setShader(envShader) ;

	return compositor ;
}

/// Function ---------------------------------

int main ()
{
	// Prepare for logging
	std::unique_ptr<nkLog::Logger> logger = std::make_unique<nkLog::ConsoleLogger>() ;
	nkGraphics::LogManager::getInstance()->setReceiver(logger.get()) ;

	// For easiness
	nkResources::ResourceManager::getInstance()->setWorkingPath("Data") ;

	// Initialize and create context with window
	if (!nkGraphics::System::getInstance()->initialize())
		return -1 ;

	nkGraphics::RenderContext* context = nkGraphics::RenderContextManager::getInstance()->createRenderContext(nkGraphics::RenderContextDescriptor(800, 600, false, true)) ;

	baseInit() ;

	// Prepare the texture we will use
	prepareTexture() ;
	prepareSampler() ;

	// Change its shader
	prepareProgram() ;
	prepareShader() ;
	assignShader() ;

	prepareEnvProgram() ;
	prepareEnvShader() ;

	// Prepare the composition
	nkGraphics::Compositor* compositor = prepareCompositor() ;
	
	// Use the compositor for the context we just created
	context->setCompositor(compositor) ;

	// And trigger the rendering
	renderLoop(context) ;

	// Clean exit
	nkGraphics::System::getInstance()->kill() ;
}